/*
 * JJ
 * $Id: gfw.c,v 1.2 2009/09/05 21:14:07 fred Exp $
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <err.h>
#include <errno.h>
#include <fnmatch.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#include <assert.h>

#include "gfw.h"

struct gfw_entry;
struct gfw_set {
	int               gfw_entry_cnt;
	struct gfw_entry *gfw_head;
	struct gfw_entry *gfw_tail;
};
typedef enum {ACT_UNKNOWN, ACT_ALLOW, ACT_COUNT, ACT_DENY, ACT_DROP} gfwact_t;
static const char *gfwactstr[] = 
{
	"ACT_UNKNOWN", "ACT_ALLOW", "ACT_COUNT", "ACT_DENY", "ACT_DROP", NULL
};

struct gfw_entry {
	gfwact_t	  g_action;
	int		  g_counter;
	int		  g_flags;
#define F_LOG		0x1
#define F_CANEXEC	0x2
	struct in_addr	  g_addr_to;
	struct in_addr	  g_mask_from;
	unsigned short	  g_mask_from_b;
	char	         *g_dir;
	struct gfw_entry *g_next;
};
typedef enum {
	T_ERR, T_ACTION, T_ADDR_FROM, T_ADDR_TO, T_DIRECTORY, 
	T_LOG, T_CANEXEC, T_END
} tok_t;

static char   **p_getwords(char *, int *);
static tok_t    p_gettok(char **, int *);
static gfwact_t p_action_type(char *);
static struct gfw_set *gfw_newset();
static struct gfw_entry *gfw_add_entry(struct gfw_set *, struct gfw_entry *);
static int gfw_dirmatch(const char *, char *);
static struct gfw_set	*global_set;
static struct gfw_entry *gfw_match(struct gfw_set *, struct in_addr *, char *, unsigned short *);
static int lineno;
/*
 * RULE -> action | action IP | action DIRPATH | action IP DIRPATH
 * action IP [ipaddr] TO DIRPATH
 * allow ip 172.17.7.3 to "dir" ;
 * count ip 172.17.7.3 to "dir" ;
 * deny ip 172.17.7.3 to "dir" ;
 * drop ip 172.17.7.3 ;
*/

extern void
gfw_free()
{
	struct gfw_entry	*ge;
	struct gfw_entry	*gen;

	if (global_set != NULL) {
		ge = global_set->gfw_head;
		while (ge != NULL) {
			gen = ge->g_next;
			free(ge->g_dir);
			free(ge);
			ge = gen;
		}
		free(global_set);
		global_set = NULL;
	}
	return;
}

extern int
gfw_init(const char *path)
{
	char	*buffer;
	char	**wlist;
	int	wordcnt;
	int	curw;
	struct gfw_entry gentry;
	tok_t	curtok;
	FILE	*gfw_rules;
#define CURW_INCR(curwa, incr) if (wordcnt > curwa) curwa += incr
#define P_ERR(msg)			\
	fprintf(stderr, msg);		\
	curtok = T_ERR;			\
	continue;

	global_set = gfw_newset();
	buffer = calloc(BUFSIZ*2, sizeof(char));
	wordcnt = 0;
	curw = 0;
	if ((gfw_rules = fopen(path, "r")) == NULL)
		return (-1);
	while (fgets(buffer, BUFSIZ*2, gfw_rules) != NULL) {
		lineno++;
		wlist = p_getwords(buffer, &wordcnt);
		if (global_set->gfw_entry_cnt < 0)
			assert(global_set->gfw_entry_cnt >= 0 && "memory corruption after getwords");
		curw = 0;
		curtok = T_ACTION;
		bzero(&gentry, sizeof(gentry));
		assert(((global_set->gfw_tail != NULL && global_set->gfw_head != NULL) ||
			(global_set->gfw_tail == NULL && global_set->gfw_head == NULL)) &&
				"gfw_tail -> NULL!?! @tokloop");
		while (curtok != T_END && curtok != T_ERR && wordcnt > curw) {
			if (global_set->gfw_entry_cnt < 0)
				assert(!"memory corruption!");
			switch ((curtok = p_gettok(wlist, &curw))) {
				case T_ACTION:
					if (gentry.g_action != ACT_UNKNOWN) {
						P_ERR("action specified twice or more times\n");
					}
					gentry.g_action = p_action_type(wlist[curw]);
					if (gentry.g_action == ACT_UNKNOWN) {
						P_ERR("unknown action\n");
					}
					CURW_INCR(curw, 1);
					break;
				case T_ADDR_FROM:
					if (wlist[curw] == NULL) {
						P_ERR("source address not specified\n");
					}
					gentry.g_mask_from_b = inet_net_pton(AF_INET, 
							wlist[curw + 1], 
							&gentry.g_mask_from, 
							sizeof(gentry.g_mask_from)
							);
					CURW_INCR(curw, 2);
					break;
				case T_ADDR_TO:
					if (wlist[curw] == NULL) {
						P_ERR("destination address not specified\n");
					}
					inet_pton(AF_INET, wlist[curw + 1], &gentry.g_addr_to);
					CURW_INCR(curw, 2);
					break;
				case T_DIRECTORY:
					if (gentry.g_dir != NULL) {
						P_ERR("dir already specified\n");
					}
					if (wlist[curw] == NULL) {
						P_ERR("dir path not specified\n");
					}
					gentry.g_dir = strdup(wlist[curw + 1]);
					CURW_INCR(curw, 2);
					break;
				case T_LOG:
					gentry.g_flags |= F_LOG;
					CURW_INCR(curw, 1);
					break;
				case T_CANEXEC:
					gentry.g_flags |= F_CANEXEC;
					CURW_INCR(curw, 1);
					break;
				case T_END:
					(void) gfw_add_entry(global_set, &gentry);
					errno = 0;
					break;
				case T_ERR:
				default:
					fprintf(stderr, "on %d Parser error\n", lineno);
					break;
			}
		}
		do free(wlist[wordcnt]); while (wordcnt--);
		free(wlist);
	}
	free(buffer);
	fclose(gfw_rules);
	return (0);
}


static char **
p_getwords(char *buffer, int *wcnt)
{
	char	*word;
	char	**wlist, **wret;
	size_t	wlistlen;
	size_t	wlen;
	char	*buf2;
	int	incr;

	*wcnt = 0;
	wlistlen = strlen(buffer);
	if ((buf2 = calloc(wlistlen + 100, sizeof(char))) == NULL)
		return (NULL);
	strlcpy(buf2, buffer, wlistlen + 100);
	/* buf2 is a temp XXX */
	wret = wlist = calloc(wlistlen + 100, sizeof(char *));

	for (incr=0;(word = strsep(&buf2, " \r\t")) != NULL; wlistlen--, incr=0) {
		wlen =  strlen(word);
		if (wlen > 0) {
			if (*(word+wlen-1) == '\n' && wlen > 1 /*|| *(word+wlen-2) == ';' */) {
				char str[2];
				str[0] = *(word+wlen-1);
				str[1] = '\0';
				*(word+wlen-1) = '\0';
				*(wlist+1) = strdup(str);
				(*wcnt)++;
				incr++;
			}
			/* strdup temp */
			*(wlist) = strdup(word);
			(*wcnt)++;
			incr++;
		}
		if (incr) wlist+=incr;
	}
	*(wlist+1) = NULL;
	free(buf2);
	return (wret);
}

static gfwact_t
p_action_type(char *word)
{
	gfwact_t	action;

	if (!strcasecmp(word, "deny"))
		action = ACT_DENY;
	else if (!strcasecmp(word, "allow"))
		action = ACT_ALLOW;
	else if (!strcasecmp(word, "drop"))
		action = ACT_DROP;
	else if (!strcasecmp(word, "count")) 
		action = ACT_COUNT;
	else 
		action = ACT_UNKNOWN;
	return (action);
}

static tok_t
p_gettok(char **wlist, int *curw)
{
	char	*word;
	tok_t	t;

	word = wlist[*curw];
	switch (*word) {
		case 'a': /* allow */
			t = T_ACTION;
			break;
		case 'c': /* count */
			switch (*(word+1)) {
				case 'a':
					t = T_CANEXEC;
					break;
				case 'o':
					t = T_ACTION;
					break;
				default:
					t = T_ERR;
			}
			break;
		case 'd': /* deny|dir|drop */
			switch (*(word+1)) {
				case 'e': /* deny */
					t = T_ACTION;
					break;
				case 'i': /* dir */
					t = T_DIRECTORY;
					break;
				case 'r': /* drop */
					t = T_ACTION;
					break;
				default:
					t = T_ERR;
			}
			break;
		case 'f': /* from */
			t = T_ADDR_FROM;
			break;
		case 'l': /* log */
			t = T_LOG;
			break;
		case 't': /* to */
			t = T_ADDR_TO;
			break;
		case ';':
			t = T_END;
			break;
		case '\n':
			t = T_END;
			break;
		default:
			t = T_ERR;
	}
	return (t);
}


static struct gfw_set *
gfw_newset()
{
	struct gfw_set *ret;

	ret = calloc(1, sizeof(struct gfw_set));
	return (ret);
}

static struct gfw_entry *
gfw_add_entry(struct gfw_set *gset, struct gfw_entry *entry)
{
	struct gfw_entry *e;
	assert(entry != NULL && gset != NULL && "function misuse!");

	if (entry->g_action == ACT_UNKNOWN) {
		errno = EINVAL;
		return (NULL);
	}
	if ((e = calloc(1, sizeof(struct gfw_entry))) == NULL)
		return (NULL);
	bcopy(entry, e, sizeof(struct gfw_entry));
	e->g_next = NULL;
	if (gset->gfw_head == NULL) {
		assert(e != NULL && "gfw_entry element points to NULL");
		gset->gfw_head = gset->gfw_tail = e;
	} else {
		assert(gset != NULL && "gset points to NULL?");
		assert(gset->gfw_tail != NULL && "gfw_tail points to NULL!?!");
		gset->gfw_tail->g_next = e;
		gset->gfw_tail = e;
	}
	(gset->gfw_entry_cnt)++;
	return (e);
}

#define M_ADDR		0x1
#define M_PATH		0x2

#define GFW_ALLOW	0x1
#define GFW_DONTALLOW	0x2

static int
gfw_dirmatch(const char *pattern, char *dir)
{
	int match = 0;

	if (pattern == NULL || dir == NULL)
		return (match);

	match = (fnmatch(pattern, dir, 0) == FNM_NOMATCH?0:1);
	return (match);
}

static struct gfw_entry *
gfw_match(struct gfw_set *gs, struct in_addr *from, char *path, unsigned short *match)
{
	struct gfw_entry *entry;

	assert(gs != NULL && "gs equals NULL");
	assert(from != NULL && "source address failure");
	*match = 0;
	for (entry = gs->gfw_head; entry != NULL ; entry = entry->g_next) {
		if (from != NULL && (
		    (entry->g_mask_from.s_addr>>(32-entry->g_mask_from_b)) ==
		    (from->s_addr>>(32-entry->g_mask_from_b))))
			*match |= M_ADDR;
		if (gfw_dirmatch(entry->g_dir, path))
			*match |= M_PATH;
		if (*match == M_PATH && entry->g_mask_from.s_addr == 0) break;
		else if (*match == M_ADDR && entry->g_dir == NULL) break;
		else if ((*match & (M_ADDR|M_PATH)) == (M_ADDR|M_PATH)) break;
		else if (*match == M_ADDR && entry->g_action == ACT_DROP &&
				entry->g_mask_from.s_addr != 0) break;
	}
	return (entry);
}

extern int
gfw_eval(struct in_addr *addr, char *buf)
{
	struct gfw_entry *ent;
	int	ret;
	unsigned short	mres;

	ret = 0;
	if (global_set == NULL) return (RET_PERMIT);
	if ((ent = gfw_match(global_set, addr, buf, &mres)) == NULL)
		return (RET_PERMIT);
	if (ent->g_flags & F_LOG)
		ret |= RET_LOGIT;
	if (ent->g_flags & F_CANEXEC)
		ret |= RET_CANEXEC;
	if (ent->g_action == ACT_UNKNOWN || ent->g_action == ACT_ALLOW ||
		ent->g_action == ACT_COUNT)
		ret |= RET_PERMIT;
#define IFDENY (ent->g_action == ACT_DENY)
	else if (ent->g_action == ACT_DENY && buf == NULL)
		ret |= RET_PERMIT;
	else if (ent->g_action == ACT_DENY && (buf != NULL && ent->g_dir == NULL))
		ret |= RET_DENY;
	else if  (ent->g_action == ACT_DENY && buf != NULL && 
			ent->g_dir != NULL && !strcmp(buf, ent->g_dir))
		ret |= RET_DENY;
	else if (ent->g_action == ACT_DENY && 
					(mres&(M_PATH|M_ADDR)) == (M_PATH|M_ADDR))
		ret |= RET_DENY;
	else if (ent->g_action == ACT_DENY && (!(mres & M_PATH) || !(mres & M_ADDR)))
		ret |= RET_PERMIT;
	else if (ent->g_action == ACT_DROP)
		ret |= RET_DENY;
	else
		ret |= RET_DENY;
	return (ret);
}

extern int
gfw_print(FILE *out)
{
	struct gfw_entry *entry;
	int	total;

	total = 0;
	for (entry = global_set->gfw_head; entry != NULL ; entry = entry->g_next) {
		total += fprintf((out == NULL?stdout:out), 
				"%d) %10s %8x/%2d %8x (%20s)%s%s\n",
				entry->g_counter,
				gfwactstr[entry->g_action],
				entry->g_mask_from.s_addr,
				entry->g_mask_from_b,
				entry->g_addr_to.s_addr,
				(entry->g_dir != NULL?entry->g_dir:"any dir"),
				(entry->g_flags&F_LOG?" F_LOG":""),
				(entry->g_flags&F_CANEXEC?" F_CANEXEC":"")
		       );
	}
	return (total);
}

